package uk.co.jemos.podam.typeManufacturers; import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.co.jemos.podam.api.DataProviderStrategy; import uk.co.jemos.podam.api.ObjectStrategy; import uk.co.jemos.podam.api.PodamUtils; import uk.co.jemos.podam.common.*; import javax.validation.Constraint; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import javax.xml.ws.Holder; import java.lang.annotation.Annotation; import java.lang.reflect.*; import java.util.*; import java.util.concurrent.atomic.AtomicReference; /** * Type Manufacturer utility class. * * Created by tedonema on 01/07/2015. * * @since 6.0.0.RELEASE */ public abstract class TypeManufacturerUtil { /** The application logger */ private static final Logger LOG = LoggerFactory.getLogger(TypeManufacturerUtil.class); /** * It returns a {@link AttributeStrategy} if one was specified in * annotations, or {@code null} otherwise. * * @param strategy * The data provider strategy * @param annotations * The list of annotations, irrelevant annotations will be removed * @param attributeType * Type of attribute expected to be returned * @return {@link AttributeStrategy}, if {@link PodamStrategyValue} or bean * validation constraint annotation was found among annotations * @throws IllegalAccessException * if attribute strategy cannot be instantiated * @throws InstantiationException * if attribute strategy cannot be instantiated * @throws SecurityException * if access security is violated * @throws InvocationTargetException * if invocation failed * @throws IllegalArgumentException * if illegal argument provided to a constructor */ public static AttributeStrategy<?> findAttributeStrategy(DataProviderStrategy strategy, List<Annotation> annotations, Class<?> attributeType) throws InstantiationException, IllegalAccessException, SecurityException, IllegalArgumentException, InvocationTargetException { Iterator<Annotation> iter = annotations.iterator(); while (iter.hasNext()) { Annotation annotation = iter.next(); if (annotation instanceof PodamStrategyValue) { PodamStrategyValue strategyAnnotation = (PodamStrategyValue) annotation; return strategyAnnotation.value().newInstance(); } /* Podam annotation is present, this will be handled later by type manufacturers */ if (annotation.annotationType().getAnnotation(PodamAnnotation.class) != null) { return null; } /* Find real class out of proxy */ Class<? extends Annotation> annotationClass = annotation.getClass(); if (Proxy.isProxyClass(annotationClass)) { Class<?>[] interfaces = annotationClass.getInterfaces(); if (interfaces.length == 1) { @SuppressWarnings("unchecked") Class<? extends Annotation> tmp = (Class<? extends Annotation>) interfaces[0]; annotationClass = tmp; } } Class<AttributeStrategy<?>> attrStrategyClass; if ((attrStrategyClass = strategy.getStrategyForAnnotation(annotationClass)) != null) { Constructor<AttributeStrategy<?>> ctor; try { ctor = attrStrategyClass.getConstructor(Annotation.class); return ctor.newInstance(annotation); } catch(NoSuchMethodException e) { return attrStrategyClass.newInstance(); } } if (annotation.annotationType().getAnnotation(Constraint.class) != null) { if (annotation instanceof NotNull || annotation.annotationType().getName().equals("org.hibernate.validator.constraints.NotEmpty") || annotation.annotationType().getName().equals("org.hibernate.validator.constraints.NotBlank")) { /* We don't need to do anything for NotNull constraint */ iter.remove(); } else if (!NotNull.class.getPackage().equals(annotationClass.getPackage())) { LOG.warn("Please, register AttributeStratergy for custom " + "constraint {}, in DataProviderStrategy! Value " + "will be left to null", annotation); } } else { iter.remove(); } } AttributeStrategy<?> retValue = null; if (!annotations.isEmpty() && !Collection.class.isAssignableFrom(attributeType) && !Map.class.isAssignableFrom(attributeType) && !attributeType.isArray()) { retValue = new BeanValidationStrategy(annotations, attributeType); } return retValue; } /** * Fills type agruments map * <p> * This method places required and provided types for object creation into a * map, which will be used for type mapping. * </p> * * @param typeArgsMap * a map to fill * @param pojoClass * Typed class * @param genericTypeArgs * Type arguments provided for a generics object by caller * @return Array of unused provided generic type arguments * @throws IllegalStateException * If number of typed parameters doesn't match number of * provided generic types */ public static Type[] fillTypeArgMap(final Map<String, Type> typeArgsMap, final Class<?> pojoClass, final Type[] genericTypeArgs) { TypeVariable<?>[] array = pojoClass.getTypeParameters(); List<TypeVariable<?>> typeParameters = new ArrayList<TypeVariable<?>>(Arrays.asList(array)); Iterator<TypeVariable<?>> iterator = typeParameters.iterator(); /* Removing types, which are already in typeArgsMap */ while (iterator.hasNext()) { if (typeArgsMap.containsKey(iterator.next().getName())) { iterator.remove(); } } List<Type> genericTypes = new ArrayList<Type>(Arrays.asList(genericTypeArgs)); Iterator<Type> iterator2 = genericTypes.iterator(); /* Removing types, which are type variables */ while (iterator2.hasNext()) { if (iterator2.next() instanceof TypeVariable) { iterator2.remove(); } } if (typeParameters.size() > genericTypes.size()) { String msg = pojoClass.getCanonicalName() + " is missing generic type arguments, expected " + typeParameters + ", provided " + Arrays.toString(genericTypeArgs); throw new IllegalArgumentException(msg); } int i; for (i = 0; i < typeParameters.size(); i++) { typeArgsMap.put(typeParameters.get(i).getName(), genericTypes.get(0)); genericTypes.remove(0); } Type[] genericTypeArgsExtra; if (genericTypes.size() > 0) { genericTypeArgsExtra = genericTypes.toArray(new Type[genericTypes.size()]); } else { genericTypeArgsExtra = PodamConstants.NO_TYPES; } /* Adding types, which were specified during inheritance */ Class<?> clazz = pojoClass; while (clazz != null) { Type superType = clazz.getGenericSuperclass(); clazz = clazz.getSuperclass(); if (superType instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) superType; Type[] actualParamTypes = paramType.getActualTypeArguments(); TypeVariable<?>[] paramTypes = clazz.getTypeParameters(); for (i = 0; i < actualParamTypes.length && i < paramTypes.length; i++) { if (actualParamTypes[i] instanceof Class) { typeArgsMap.put(paramTypes[i].getName(), actualParamTypes[i]); } } } } return genericTypeArgsExtra; } /** * Searches for annotation with information about collection/map size * and filling strategies * * @param strategy * a data provider strategy * @param annotations * a list of annotations to inspect * @param collectionElementType * a collection element type * @param elementStrategyHolder * a holder to pass found element strategy back to the caller, * can be null * @param keyStrategyHolder * a holder to pass found key strategy back to the caller, * can be null * @return * A number of element in collection or null, if no annotation was * found * @throws InstantiationException * A strategy cannot be instantiated * @throws IllegalAccessException * A strategy cannot be instantiated */ public static Integer findCollectionSize( DataProviderStrategy strategy, List<Annotation> annotations, Class<?> collectionElementType, Holder<AttributeStrategy<?>> elementStrategyHolder, Holder<AttributeStrategy<?>> keyStrategyHolder) throws InstantiationException, IllegalAccessException { // If the user defined a strategy to fill the collection elements, // we use it Size size = null; for (Annotation annotation : annotations) { if (annotation instanceof PodamCollection) { PodamCollection collectionAnnotation = (PodamCollection) annotation; if (null != elementStrategyHolder) { Class<? extends AttributeStrategy<?>> attributeStrategy = collectionAnnotation.collectionElementStrategy(); if (null == attributeStrategy || ObjectStrategy.class.isAssignableFrom(attributeStrategy)) { attributeStrategy = collectionAnnotation.mapElementStrategy(); } if (null != attributeStrategy) { elementStrategyHolder.value = attributeStrategy.newInstance(); } } if (null != keyStrategyHolder) { Class<? extends AttributeStrategy<?>> attributeStrategy = collectionAnnotation.mapKeyStrategy(); if (null != attributeStrategy) { keyStrategyHolder.value = attributeStrategy.newInstance(); } } return collectionAnnotation.nbrElements(); } else if (annotation instanceof Size) { size = (Size) annotation; } } Integer nbrElements = strategy .getNumberOfCollectionElements(collectionElementType); if (null != size) { if (nbrElements > size.max()) { nbrElements = size.max(); } if (nbrElements < size.min()) { nbrElements = size.min(); } } return nbrElements; } /** * Utility to merge actual types with supplied array of generic type * substitutions * * @param attributeType * actual type of object * @param genericAttributeType * generic type of object * @param suppliedTypes * an array of supplied types for generic type substitution * @param typeArgsMap * a map relating the generic class arguments ("<T, V>" for * example) with their actual types * @return An array of merged actual and supplied types with generic types * resolved */ public static Type[] mergeActualAndSuppliedGenericTypes( Class<?> attributeType, Type genericAttributeType, Type[] suppliedTypes, Map<String, Type> typeArgsMap) { TypeVariable<?>[] actualTypes = attributeType.getTypeParameters(); if (actualTypes.length <= suppliedTypes.length) { return suppliedTypes; } Type[] genericTypes = null; if (genericAttributeType instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) genericAttributeType; genericTypes = paramType.getActualTypeArguments(); } else if (genericAttributeType instanceof WildcardType) { WildcardType wildcardType = (WildcardType) genericAttributeType; genericTypes = wildcardType.getLowerBounds(); if (ArrayUtils.isEmpty(genericTypes)) { genericTypes = wildcardType.getUpperBounds(); } } List<Type> resolvedTypes = new ArrayList<Type>(); List<Type> substitutionTypes = new ArrayList<Type>(Arrays.asList(suppliedTypes)); for (int i = 0; i < actualTypes.length; i++) { Type type = null; if (actualTypes[i] instanceof TypeVariable) { type = typeArgsMap.get(((TypeVariable<?>)actualTypes[i]).getName()); } else if (actualTypes[i] instanceof WildcardType) { AtomicReference<Type[]> methodGenericTypeArgs = new AtomicReference<Type[]>(PodamConstants.NO_TYPES); type = TypeManufacturerUtil.resolveGenericParameter(actualTypes[i], typeArgsMap, methodGenericTypeArgs); } if ((type == null) && (genericTypes != null)) { if (genericTypes[i] instanceof Class) { type = genericTypes[i]; } else if (genericTypes[i] instanceof WildcardType) { AtomicReference<Type[]> methodGenericTypeArgs = new AtomicReference<Type[]>(PodamConstants.NO_TYPES); type = resolveGenericParameter(genericTypes[i], typeArgsMap, methodGenericTypeArgs); } else if (genericTypes[i] instanceof ParameterizedType) { type = genericTypes[i]; } else { LOG.debug("Skipping type {} {}", actualTypes[i], genericTypes[i]); } } if (type != null) { resolvedTypes.add(type); if (!substitutionTypes.isEmpty() && substitutionTypes.get(0).equals(type)) { substitutionTypes.remove(0); } } } Type[] resolved = resolvedTypes.toArray(new Type[resolvedTypes.size()]); Type[] supplied = substitutionTypes.toArray(new Type[substitutionTypes.size()]); return ArrayUtils.addAll(resolved, supplied); } /** * It resolves generic parameter type * * * @param paramType * The generic parameter type * @param typeArgsMap * A map of resolved types * @param methodGenericTypeArgs * Return value posible generic types of the generic parameter * type * @return value for class representing the generic parameter type */ public static Class<?> resolveGenericParameter(Type paramType, Map<String, Type> typeArgsMap, AtomicReference<Type[]> methodGenericTypeArgs) { Class<?> parameterType = null; //Safe copy Map<String, Type> localMap = new HashMap<String, Type>(typeArgsMap); methodGenericTypeArgs.set(PodamConstants.NO_TYPES); if (paramType instanceof Class) { parameterType = (Class<?>) paramType; } else if (paramType instanceof TypeVariable<?>) { final TypeVariable<?> typeVariable = (TypeVariable<?>) paramType; final Type type = localMap.get(typeVariable.getName()); if (type != null) { parameterType = resolveGenericParameter(type, localMap, methodGenericTypeArgs); } } else if (paramType instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) paramType; parameterType = (Class<?>) pType.getRawType(); Type[] actualTypeArgs = pType.getActualTypeArguments(); if (!typeArgsMap.isEmpty()) { for (int i = 0; i < actualTypeArgs.length; i++) { Class<?> tmp = resolveGenericParameter(actualTypeArgs[i], localMap, methodGenericTypeArgs); if (tmp != actualTypeArgs[i]) { /* If actual type argument has its own arguments, * we will loose them now, so we will leave type unresolved * until lower levels of type resolution */ if (ArrayUtils.isEmpty(methodGenericTypeArgs.get())) { actualTypeArgs[i] = tmp; } } } } methodGenericTypeArgs.set(actualTypeArgs); } else if (paramType instanceof WildcardType) { WildcardType wType = (WildcardType) paramType; Type[] bounds = wType.getLowerBounds(); String msg; if (ArrayUtils.isNotEmpty(bounds)) { msg = "Lower bounds:"; } else { bounds = wType.getUpperBounds(); msg = "Upper bounds:"; } if (ArrayUtils.isNotEmpty(bounds)) { LOG.debug(msg + Arrays.toString(bounds)); parameterType = resolveGenericParameter(bounds[0], localMap, methodGenericTypeArgs); } } if (parameterType == null) { LOG.warn("Unrecognized type {}. Will use Object instead", paramType); parameterType = Object.class; } return parameterType; } /** * It retrieves the value for the {@link PodamStrategyValue} annotation with * which the attribute was annotated * * @param attributeType * The attribute type, used for type checking * @param attributeStrategy * The {@link AttributeStrategy} to use * @return The value for the {@link PodamStrategyValue} annotation with * which the attribute was annotated * @throws IllegalArgumentException * If the type of the data strategy defined for the * {@link PodamStrategyValue} annotation is not assignable to * the annotated attribute. This de facto guarantees type * safety. */ public static Object returnAttributeDataStrategyValue(Class<?> attributeType, AttributeStrategy<?> attributeStrategy) throws IllegalArgumentException { if (null == attributeStrategy) { return null; } Object retValue = attributeStrategy.getValue(); if (retValue != null) { Class<?> desiredType = attributeType.isPrimitive() ? PodamUtils.primitiveToBoxedType(attributeType) : attributeType; if (!desiredType.isAssignableFrom(retValue.getClass())) { String errMsg = "The AttributeStrategy " + attributeStrategy.getClass().getName() + " produced value of type " + retValue.getClass().getName() + " incompatible with attribute type " + attributeType.getName(); throw new IllegalArgumentException(errMsg); } else { LOG.debug("The parameter {} will be filled using the following strategy {}", attributeType, attributeStrategy); } } return retValue; } }